/*
 * Copyright 2021 The Chromium Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */
package io.flutter.utils;

import com.intellij.lang.ASTNode;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.impl.source.tree.AstBufferUtil;
import com.intellij.util.TripleFunction;
import com.jetbrains.lang.dart.DartTokenTypes;
import com.jetbrains.lang.dart.psi.DartComponent;
import com.jetbrains.lang.dart.psi.DartFile;
import io.flutter.FlutterUtils;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontFormatException;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.font.FontRenderContext;
import java.awt.font.LineMetrics;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Paths;
import java.util.Properties;
import javax.imageio.ImageIO;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

@SuppressWarnings("UseJBColor")
public class IconPreviewGenerator {
  private static final Logger LOG = Logger.getInstance(IconPreviewGenerator.class);

  @NotNull final String fontFilePath;
  int iconSize = 16;
  int fontSize = 16;
  @NotNull Color fontColor = Color.gray;

  public IconPreviewGenerator(@NotNull String fontFilePath) {
    this.fontFilePath = fontFilePath;
  }

  public IconPreviewGenerator(@NotNull String fontFilePath, int iconSize, int fontSize, @Nullable Color fontColor) {
    this(fontFilePath);
    this.iconSize = iconSize;
    this.fontSize = fontSize;
    if (fontColor != null) {
      this.fontColor = fontColor;
    }
  }

  public Icon convert(String number) {
    int codepoint;
    if (number.startsWith("0x") || number.startsWith("0X")) {
      number = number.substring(2);
      codepoint = Integer.parseInt(number, 16);
    }
    else {
      codepoint = Integer.parseInt(number, 10);
    }
    return convert(codepoint);
  }

  public Icon convert(int code) {
    return runInGraphicsContext((BufferedImage image, Graphics2D graphics, FontRenderContext frc) -> {
      char ch = Character.toChars(code)[0];
      String codepoint = Character.toString(ch);

      drawGlyph(codepoint, graphics, frc);
      return new ImageIcon(image);
    });
  }

  // Given a file at path-to-font-properties in the format generated by tools_metadata (on github),
  // to generate double-size icons for all glyphs in a font:
  // IconPreviewGenerator ipg = new IconPreviewGenerator("path-to-ttf-file", 32, 32, Color.black);
  // ipg.batchConvert("path-to-out-dir", "path-to-font-properties", "@2x");
  public void batchConvert(@NotNull String outPath, @NotNull String path, @NotNull String suffix) {
    final String outputPath = outPath.endsWith("/") ? outPath : outPath + "/";
    //noinspection ResultOfMethodCallIgnored
    new File(outputPath).mkdirs();

    runInGraphicsContext((BufferedImage image, Graphics2D graphics, FontRenderContext frc) -> {
      Properties fontMap = new Properties();
      File file = new File(path);
      try (InputStream stream = new BufferedInputStream(new FileInputStream(file))) {
        fontMap.load(stream);
        for (Object nextKey : fontMap.keySet()) {
          if (!(nextKey instanceof String)) continue;
          String codepoint = (String)nextKey;
          if (!codepoint.endsWith(".codepoint")) continue;
          String iconName = fontMap.getProperty(codepoint);
          codepoint = codepoint.substring(0, codepoint.indexOf(".codepoint"));
          int code = Integer.parseInt(codepoint, 16);
          char ch = Character.toChars(code)[0];
          codepoint = Character.toString(ch);

          drawGlyph(codepoint, graphics, frc);
          ImageIO.write(image, "PNG", new File(outputPath + iconName + suffix + ".png"));
        }
      }
      catch (IOException ex) {
        FlutterUtils.warn(LOG, ex);
      }
      return null;
    });
  }

  private Icon runInGraphicsContext(TripleFunction<BufferedImage, Graphics2D, FontRenderContext, Icon> callback) {
    Icon result = null;
    Graphics2D graphics = null;
    //noinspection UndesirableClassUsage
    BufferedImage image = new BufferedImage(iconSize, iconSize, BufferedImage.TYPE_4BYTE_ABGR);
    try (InputStream inputStream = new FileInputStream(fontFilePath)) {
      Font font = Font.createFont(Font.TRUETYPE_FONT, inputStream).deriveFont(Font.PLAIN, fontSize);
      graphics = image.createGraphics();
      graphics.setFont(font);
      graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
      FontRenderContext frc = new FontRenderContext(new AffineTransform(), true, true);
      result = callback.fun(image, graphics, frc);
    }
    catch (IOException | FontFormatException ex) {
      FlutterUtils.warn(LOG, ex);
    }
    finally {
      if (graphics != null) graphics.dispose();
    }
    return result;
  }

  private void drawGlyph(String codepoint, Graphics2D graphics, FontRenderContext frc) {
    Font font = graphics.getFont();
    Rectangle2D rect = font.getStringBounds(codepoint, frc);
    LineMetrics metrics = font.getLineMetrics(codepoint, frc);
    float lineHeight = metrics.getHeight();
    float ascent = metrics.getAscent();
    float width = (float)rect.getWidth();
    float x0 = (iconSize - width) / 2.0f;
    float y0 = (iconSize - lineHeight) / 2.0f + ascent + (lineHeight - ascent) / 2.0f;

    graphics.setComposite(AlphaComposite.Clear);
    graphics.fillRect(0, 0, iconSize, iconSize);

    graphics.setComposite(AlphaComposite.Src);
    graphics.setColor(fontColor);
    graphics.drawString(codepoint, x0, y0);
  }

  @Nullable
  public static VirtualFile findAssetMapFor(@NotNull DartComponent dartClass) {
    final ASTNode node = dartClass.getNode();
    if (DartTokenTypes.CLASS_DEFINITION != node.getElementType()) {
      return null;
    }
    final DartFile psiElement = (DartFile)node.getPsi().getParent();
    final VirtualFile file = psiElement.getVirtualFile();
    VirtualFile map = findAssetMapIn(file.getParent());
    if (map != null) {
      return map;
    }
    final PsiElement identifier = dartClass.getNameIdentifier();
    if (identifier == null) {
      return null;
    }
    final String className = AstBufferUtil.getTextSkippingWhitespaceComments(identifier.getNode());
    final URL resource = IconPreviewGenerator.class.getResource("/iconAssetMaps/" + className + "/asset_map.yaml");
    if (resource == null) {
      return null;
    }
    try {
      final URI uri = resource.toURI();
      return LocalFileSystem.getInstance().findFileByNioFile(Paths.get(uri));
    }
    catch (URISyntaxException e) {
      return null;
    }
  }

  @Nullable
  public static VirtualFile findAssetMapIn(@NotNull VirtualFile dartClass) {
    VirtualFile dir = dartClass.getParent();
    VirtualFile preview = null;
    while (dir != null && !dir.getName().equals("lib")) {
      preview = dir.findChild("ide_preview");
      if (preview == null) {
        dir = dir.getParent();
      }
      else {
        dir = null;
      }
    }
    if (preview == null && dir != null && dir.getName().equals("lib")) {
      dir = dir.getParent();
      preview = dir.findChild("ide_preview");
      if (preview == null) {
        return null;
      }
    }
    return preview == null ? null : preview.findChild("asset_map.yaml");
  }
}
